JWT数字签名与token实现

您所在的位置:网站首页 golang jwt公钥验证 JWT数字签名与token实现

JWT数字签名与token实现

2023-07-06 22:09| 来源: 网络整理| 查看: 265

JWT介绍 官方介绍

JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。

什么时候你应该用JSON Web Token ?

下列场景中使用JSON Web Token是很有用的:

Authorization (授权) : 这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。Information Exchange (信息交换) : 对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式。因为JWT可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。 在这里插入图片描述 博主讲解 JWT具体用来干什么的?

JSON Web Token (JWT) 就是用来保证数据不可篡改,鉴定消息无篡改;绝不是保证消息保密性的东西;这一点要特别清楚,首先清楚JWT具体用来干什么的,从而能够理解其设计原理;经过jwt加密的消息是公开可见的,是不可篡改的,因此jwt的消息体中payload的数据不要放敏感数据,如果需要放敏感数据,请加密。例如payload中可以存放用户名,密码的话需要加密后在存放;

明白了上述用途,再去了解原理才会融会贯通,下面讲解一下非对称加密;

非对称加密

非对称加密(如RSA)是通过公钥和私钥进行加密,解密的过程;私钥只能被一方所拥有,一般是服务器;公钥可以公开,任何人可以获取公钥;通过公钥加密的消息只能通过私钥解密,通过私钥加密的消息也只能通过公钥解密;但是这个过程不是线性的,使用公钥加密和私钥加密的密文是不一样的;

公钥加密,私钥解密

这种情况用于消息的加密,保证消息的安全性,即保证消息的不可见性,保密性,敏感性;一般是客户端向服务器发送消息时为了避免消息被泄露,使用服务器公开的公钥进行消息加密形成密文发给服务器,只有服务器使用私钥才能解密密文获得消息。

私钥加密,公钥解密

使用私钥加密,可以作为数字签名防止伪造,但是无法保证数据的不可见性;因为私钥加密的密文可以被其他用户使用公钥解密,其他用户也就可以获取消息,如果要避免敏感消息被盗取,需要将敏感消息加密后放在jwt的payload中。

小常识

遗憾的是,公钥算法速度非常慢,大约比对称算法慢 1000 倍。 使用它们来加密大量数据是不切实际的。 实际上,公钥算法用于加密 会话密钥。 对称算法 用于加密/解密大多数数据。由于对消息进行签名实际上会加密消息,因此使用公钥签名算法对大型消息进行签名是不切实际的。

JWT的数据结构

JWT其实就是一个很长的字符串,字符之间通过"."分隔符分为三个子串,各字串之间没有换行符。每一个子串表示了一个功能块,总共有三个部分:JWT头(header)、有效载荷(payload)、签名(signature),如下图所示:

在这里插入图片描述

JWT头

JWT头是一个描述JWT元数据的JSON对象,通常如下所示:

{"alg": "HS256","typ": "JWT"}

alg:表示签名使用的算法,默认为HMAC SHA256(写为HS256)

typ:表示令牌的类型,JWT令牌统一写为JWT

最后,使用Base64 URL算法将上述JSON对象转换为字符串,注意Base64 URL只是编码算法,不是加密算法,可以被解码得到数据。

有效载荷

有效载荷,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。

有效载荷部分规定有如下七个默认字段供选择:

iss:发行人 exp:到期时间 sub:主题 aud:用户 nbf:在此之前不可用 iat:发布时间 jti:JWT ID用于标识该JWT

除以上默认字段外,还可以自定义私有字段。

userid:100001 username:nick email:[email protected]

最后,同样使用Base64 URL算法将有效载荷部分JSON对象转换为字符串,注意Base64 URL只是编码算法,不是加密算法,可以被解码得到数据。

签名

签名实际上是一个使用私钥加密的过程,是对上面两部分数据通过指定的算法生成哈希,以确保数据不会被篡改。

首先需要指定一个密码(secret,可以服务器随机设置,相当于种子),该密码仅仅保存在服务器中,并且不能向用户公开。然后使用JWT头中指定的签名算法(默认情况下为HMAC SHA256),根据以下公式生成签名哈希:为了得到签名部分,你必须有编码过的header、编码过的payload、一个密码,签名算法是header中指定的那个,然对它们签名即可。

HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )

在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串 ,每个部分用"."分隔,就构成整个JWT对象。

xxxxx.yyyyy.zzzzz,xxxxx是header经过Base64 URL编码后的字符串,yyyyy是payload经过Base64 URL编码后的字符串,zzzzz是对xxxxx.yyyyy经过私钥加密后得到的字符串;

JWT签名算法

JWT签名算法中,一般有两个选择:HS256和RS256。

HS256 (带有 SHA-256 的 HMAC )是一种对称加密算法, 双方之间仅共享一个密钥,没有公钥私钥之分。由于使用相同的密钥生成签名和验证签名, 因此必须注意确保密钥不被泄密。

RS256 (采用SHA-256 的 RSA 签名) 是一种非对称加密算法, 它使用公共/私钥对: JWT的提供方采用私钥生成签名, JWT 的使用方获取公钥以验证签名。

token认证

在讲token认证之前,我们先回顾一下以前熟悉的seesion认证,Cookie-session 认证机制是通过浏览器带上来Cookie对象来与服务器端的session对象匹配来实现状态管理。第一次请求认证在服务端创建一个Session对象,同时在用户的浏览器端创建了一个Cookie对象;当我们关闭浏览器的时候,cookie会被删除。但可以通过修改cookie 的expire time使cookie在一定时间内有效。session 认证的缺点其实很明显,由于 session 是保存在服务器里,所以如果分布式部署应用的话,会出现session不能共享的问题,很难扩展。最大的问题是session将用户信息存储在了服务器上,这样会导致服务器存储压力。而token相反,把认证签名存放在客户端,客户端访问只需要带上token令牌签名即可访问自己的账户;

JSON Web Tokens是如何工作的

在认证的时候,当用户用他们的凭证成功登录以后,一个JSON Web Token将会被返回。此后,token就是用户凭证了,你必须非常小心以防止出现安全问题。一般而言,你保存令牌的时候不应该超过你所需要它的时间。HTTP协议是无状态的,也就是说,如果我们已经认证了一个用户,那么他下一次请求的时候,服务器不知道我是谁,我们必须再次认证。传统的做法是将已经认证过的用户信息存储在服务器上,比如Session。用户下次请求的时候带着Session ID,然后服务器以此检查用户是否认证过。Sessions : 每次用户认证通过以后,服务器需要创建一条记录保存用户信息,通常是在内存中,随着认证通过的用户越来越多,服务器的在这里的开销就会越来越大。 JWT与Session的差异 相同点是,它们都是存储用户信息;然而,Session是在服务器端的,而JWT是在客户端的。

Session方式存储用户信息的最大问题在于要占用大量服务器内存,增加服务器的开销。

而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。

Session的状态是存储在服务器端,客户端只有session id;而Token的状态是存储在客户端。

基于Token的身份认证是如何工作的 基于Token的身份认证是无状态的,服务器或者Session中不会存储任何用户信息。没有会话信息意味着应用程序可以根据需要扩展和添加更多的机器,而不必担心用户登录的位置。

虽然这一实现可能会有所不同,但其主要流程如下:

用户携带用户名和密码请求访问 ,第一次访问必须使用账号密码;

服务器校验用户凭据,验证账号密码;

应用提供一个token给客户端,通过后生成jwt数据xxxxx.yyyyy.zzzzz作为token;

客户端存储token,并且在随后的每一次请求中都带着它,token用于验证用户身份;

服务器校验token并返回数据,如何验证呢?只需要使用私钥对xxxxx.yyyyy进行加密,然后得到的结果与zzzzz对比即可,通过后对yyyyy进行base64解码获得用户id,即证明客户端是上一次访问的用户,且数据未被篡改;如果被篡改了,那么xxxxx.yyyyy加密不能得到zzzzz。

每一次请求都需要token,Token应该放在请求header中 ,我们还需要将服务器设置为接受来自所有域的请求。

其他

用Token的好处 - 无状态和可扩展性:Tokens存储在客户端。完全无状态,可扩展。我们的负载均衡器可以将用户传递到任意服务器,因为在任何地方都没有状态或会话信息。 - 安全:Token不是Cookie。(The token, not a cookie.)每次请求的时候Token都会被发送。而且,由于没有Cookie被发送,还有助于防止CSRF攻击。即使在你的实现中将token存储到客户端的Cookie中,这个Cookie也只是一种存储机制,而非身份认证机制。没有基于会话的信息可以操作,因为我们没有会话!

还有一点,token在一段时间以后会过期,这个时候用户需要重新登录。这有助于我们保持安全。还有一个概念叫token撤销,它允许我们根据相同的授权许可使特定的token甚至一组token无效。JWT与OAuth的区别 -OAuth2是一种授权框架 ,JWT是一种认证协议 -无论使用哪种方式切记用HTTPS来保证数据的安全性 -OAuth2用在使用第三方账号登录的情况(比如使用weibo, qq, github登录某个app),而JWT是用在前后端分离, 需要简单的对后台API进行保护时使用。

实战jwt

jjwt是一个提供JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。

导入依赖 io.jsonwebtoken jjwt 0.9.1 junit junit 4.12 cn.hutool hutool-all 5.1.0 实战案例 package com.example.test; import cn.hutool.core.io.FileUtil; import io.jsonwebtoken.*; import io.jsonwebtoken.SignatureException; import org.junit.Test; import java.security.*; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.HashMap; public class JwtTest { /** * 对称加密算法 */ @Test public void test_HS256(){ String key = "8888"; //秘钥要大于3个字符,至少四个字符 // 封装jwt的head部信息 HashMap header = new HashMap(); // SignatureAlgorithm.HS256.getValue() 使用HS256非对称加密算法 header.put("arg",SignatureAlgorithm.HS256.getValue()); //指定加密算法,不使用加密算法的话设置value为none即可 header.put("typ","JWT"); //指定令牌类型 // 封装jwt的body部分 HashMap body = new HashMap(); body.put("account","15637283927"); body.put("phone","123456"); body.put("role","user"); // 生成JWT令牌 String token = Jwts.builder() .setHeader(header) .setClaims(body) .setId("00001") //设置这个jwt的唯一标识,不写也可以 .signWith(SignatureAlgorithm.HS256,key) //使用公钥加密 .compact(); // 打印生成的jwt令牌token System.out.println(token); String[] split = token.split("\\."); System.out.println("header:"+split[0]); System.out.println("body:"+split[1]); // 解析jwt令牌,使用服务器的私钥解密 Jwt res = null; try { res = Jwts .parser() .setSigningKey(key) .parse(token); Header header_parser = res.getHeader(); Object body_parser = res.getBody(); System.out.println(header_parser); System.out.println(body_parser); } catch (SignatureException e) { System.out.println("秘钥错误!"); } } /** * 生成公钥和私钥 * @param seedPassword 提供种子密码 * @return * @throws Exception */ //生成自己的 秘钥/公钥 对 public byte[][] GeneratPubPriKey(String seedPassword) throws Exception{ // 使用RSA算法,获得RSA算法实例 KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); SecureRandom secureRandom = new SecureRandom(seedPassword.getBytes()); // 使用种子密码初始化 keyPairGenerator.initialize(1024, secureRandom); // 获得密码对,编码为byte类型 KeyPair keyPair = keyPairGenerator.genKeyPair(); byte[] publicKeyBytes = keyPair.getPublic().getEncoded(); byte[] privateKeyBytes = keyPair.getPrivate().getEncoded(); //存入本地文件,可以使用最后面提供的代码读取秘钥文件,这里作为例子直接返回 // FileUtil.writeBytes(publicKeyBytes, "d:\\pub.key"); // FileUtil.writeBytes(privateKeyBytes, "d:\\pri.key"); byte[][] key = new byte[][]{publicKeyBytes,privateKeyBytes}; return key; } /** * 非对称加密算法:实现的是私钥加密,公钥解密 * 一般用于身份识别,电子签名,确认加密的数据属于公钥下发方 * 能用你的公钥解密,那么数据一定是你的(一定使用你的私钥加密的) * 用于识别服务器身份 */ @Test public void test_RS256() throws Exception { // 封装jwt的head部信息 HashMap header = new HashMap(); // SignatureAlgorithm.HS256.getValue() 使用HS256非对称加密算法 header.put("arg",SignatureAlgorithm.RS256.getValue()); //指定加密算法,不使用加密算法的话设置value为none即可 header.put("typ","JWT"); //指定令牌类型 // 封装jwt的body部分 HashMap body = new HashMap(); body.put("account","mimiNick"); body.put("phone","123456"); body.put("role","user"); // 获取公钥和私钥 byte[][] keys = GeneratPubPriKey("8888888"); // 获取私钥对象 byte[] priKeyByte = keys[1]; PKCS8EncodedKeySpec spec2 = new PKCS8EncodedKeySpec(priKeyByte); KeyFactory kf = KeyFactory.getInstance("RSA"); PrivateKey privateKey = kf.generatePrivate(spec2); // 生成JWT令牌 String token = Jwts.builder() .setHeader(header) .setClaims(body) .setId("00001") //设置这个jwt的唯一标识,不写也可以 .signWith(SignatureAlgorithm.RS256,privateKey) //使用私钥加密 .compact(); // 打印生成的jwt令牌token System.out.println(token); String[] split = token.split("\\."); System.out.println("header:"+split[0]); System.out.println("body:"+split[1]); //获取公钥 byte[] publicByte = keys[0]; X509EncodedKeySpec spec = new X509EncodedKeySpec(publicByte); PublicKey publicKey = kf.generatePublic(spec); // 解析jwt令牌,使用服务器的私钥解密 Jwt res = null; try { res = Jwts .parser() .setSigningKey(publicKey) //使用公钥解密 .parse(token); Header header_parser = res.getHeader(); Object body_parser = res.getBody(); System.out.println(header_parser); System.out.println(body_parser); } catch (SignatureException e) { System.out.println("秘钥错误!"); } } @Test public void test_RS256_2() throws Exception { // 封装jwt的head部信息 HashMap header = new HashMap(); // SignatureAlgorithm.HS256.getValue() 使用HS256非对称加密算法 header.put("arg",SignatureAlgorithm.RS256.getValue()); //指定加密算法,不使用加密算法的话设置value为none即可 header.put("typ","JWT"); //指定令牌类型 // 封装jwt的body部分 HashMap body = new HashMap(); body.put("account","mimiNick"); body.put("phone","123456"); body.put("role","user"); // 获取公钥和私钥 byte[][] keys = GeneratPubPriKey("8888888"); // 获取私钥对象 byte[] priKeyByte = keys[1]; PKCS8EncodedKeySpec spec2 = new PKCS8EncodedKeySpec(priKeyByte); KeyFactory kf = KeyFactory.getInstance("RSA"); PrivateKey privateKey = kf.generatePrivate(spec2); //获取公钥 byte[] publicByte = keys[0]; X509EncodedKeySpec spec = new X509EncodedKeySpec(publicByte); PublicKey publicKey = kf.generatePublic(spec); // 生成JWT令牌 String token = Jwts.builder() .setHeader(header) .setClaims(body) .setId("00001") //设置这个jwt的唯一标识,不写也可以 .signWith(SignatureAlgorithm.RS256,privateKey) //使用私钥加密 .compact(); // 打印生成的jwt令牌token System.out.println(token); String[] split = token.split("\\."); System.out.println("header:"+split[0]); System.out.println("body:"+split[1]); // 解析jwt令牌,使用服务器的私钥解密 Jwt res = null; try { res = Jwts .parser() .setSigningKey(publicKey) //使用公钥解密 .parse(token); Header header_parser = res.getHeader(); Object body_parser = res.getBody(); System.out.println(header_parser); System.out.println(body_parser); } catch (SignatureException e) { System.out.println("秘钥错误!"); } } } 获取磁盘上的秘钥文件代码 //获取私钥 public PrivateKey getPriKey() throws Exception{ InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("pri.key"); DataInputStream dis = new DataInputStream(resourceAsStream); byte[] keyBytes = new byte[resourceAsStream.available()]; dis.readFully(keyBytes); PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes); KeyFactory kf = KeyFactory.getInstance("RSA"); return kf.generatePrivate(spec); } //获取公钥 public PublicKey getPubKey() throws Exception{ InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("pub.key"); DataInputStream dis = new DataInputStream(resourceAsStream); byte[] keyBytes = new byte[resourceAsStream.available()]; dis.readFully(keyBytes); X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); KeyFactory kf = KeyFactory.getInstance("RSA"); return kf.generatePublic(spec); }


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3